Julia数据科学系列-Queryverse系列包
Query.jl
基本上可以对Julia中绝大部分可迭代数据类型(支持TableTraits.jl的数据类型, 涵盖了所有IterableTables.jl中的类型)做处理, 包括filter, project, join, sort, group
等, Query受C语言的LINQ
和R语言的dplyr
启发。
支持几乎所有
C#规范
的查询表达式(LINQ), 还添加了额外的julia特有功能支持大量数据源:
DataFrames.jl Pandas.jl IndexedTables.jl JuliaDB.jl TimeSeries.jl Temporal.jl CSVFiles.jl ExcelFiles.jl FeatherFiles.jl ParquetFiles.jl BedgraphFiles.jl StatFiles.jl DifferentialEquations(任何 DESolution) 数组和任何可以迭代的类型
处理结果可以具现化为多种数据结构:
迭代器 DataFrames.jl IndexedTables.jl JuliaDB.jl TimeSeries.jl Temporal.jl Pandas.jl StatsModels.jl CSVFiles.jl FeatherFiles.jl ExcelFiles.jl StatPlots.jl VegaLite.jl TableView.jl DataVoyager.jl 字典或任何数组
一次处理可混合多个源的数据, 比如对
DataFrame
和CSV文件
进行join
操作针对 DataFrames 的查询是完全类型稳定的
提供三种API用来让包开发者使用Query.jl查询:
最简单的API: 只需作者提供一个迭代器
提供查询的完整图表示
query graph
提供自己的数据结构, 表示一个
query graph
简介
Query.jl支持两种查询语法:
独立查询操作(Standalone query operators)
LINQ样式的查询操作
独立查询操作
通过管道运算符组合成复杂的查询:
using Query, DataFrames
df = DataFrame(name=["John", "Sally", "Kirk"],
age=[23., 42., 59.],
children=[3,5,2])
x = df |>
@filter(_.age>50) |>
@map({_.name, _.children}) |>
DataFrame
println(x)
LINQ样式查询
q = @from <range variable> in <source> begin
<query statements>
end
多个查询条件用换行符分隔, 上述查询的LINQ样式为:
x = @from i in df begin
@where i.age>50
@select {i.name, i.children}
@collect DataFrame
# 注意LINQ中用@collect 定义输出结果的格式
end
println(x)
LINQ查询也可以管道, 使用@query(变量, 查询条件块)
:
x = df |> @query(i, begin
@where i.age>50
@select {i.name, i.children}
end) |> DataFrame
println(x)
表和缺失值
查询类似表的结构时, 数据被视为
NamedTuple
, Query中的{}
语法用于方便构造NamedTuple
缺失值被当作是
DataValue
类型(DataValues.jl)所有运算符和比较符都自动处理缺失
如果使用的函数本身不支持缺失值的处理, 可以用
.
进行运算符提升
独立查询运算符
宏@map
element_selector
必须是匿名函数, 接收单个元素
宏@filter
filter_condition
必须是匿名函数, 返回Boolean类型
宏@groupby
简单形式:
key_selector
是匿名函数, 为每个输入元素返回一个分组值
变体形式:
element_selector
: 匿名投影函数, 将元素分组之前应用该函数
@groupby
通常和@map
一起使用, 按照分组进行操作, 生成新的数据。
宏@orderby,@orderbydescending,@thenby,@thenbydescending
数据排序操作, 排序必须以@orderby或@orderby_descending
开始, 后边可以接多个@thenby,@thenby_descending
key_selector
:匿名函数, 根据返回的值进行排序
宏@groupjoin
outer|inner
: 任何可查询的源 outer_selector|inner_selector
: 匿名函数, 从指定源中提取值 result_selector
: 匿名函数, 接受两个参数, 分别来自两个源
例子:
df1 = DataFrame(a=[1,2,3], b=[1.,2.,3.])
df2 = DataFrame(c=[2,4,2], d=["John", "Jim", "Sally"])
x = df1 |> @groupjoin(df2, _.a, _.c, {t1=_.a, t2=length(__)}) |> DataFrame
println(x)
宏@join
命令格式同@groupjoin
df1 = DataFrame(a=[1,2,3], b=[1.,2.,3.])
df2 = DataFrame(c=[2,4,2], d=["John", "Jim","Sally"])
x = df1 |> @join(df2, _.a, _.c, {_.a, _.b, __.c, __.d}) |> DataFrame
println(x)
df1 = DataFrame(a=[1,2,3], b=[1.,2.,3.])
df2 = DataFrame(c=[2,4,2], d=["John", "Jim","Sally"])
x = df1 |> @groupjoin(df2, _.a, _.c, {A=_, B=__}) |> DataFrame # groupjoin
y = df1 |> @join(df2, _.a, _.c, {A=_, B=__}) |> DataFrame # join
julia> x.B # 输出结果跟df1的行数相同, 没有交集的行也输出了
3-element Vector{Vector{NamedTuple{(:c, :d), Tuple{Int64, String}}}}:
[]
[(c = 2, d = "John"), (c = 2, d = "Sally")]
[]
julia> y.B # 只输出了有交集的行, 且重复的记录逐行输出
2-element Vector{NamedTuple{(:c, :d), Tuple{Int64, String}}}:
(c = 2, d = "John")
(c = 2, d = "Sally")
宏@mapmany
collection_selector
: 匿名函数, 接受一个参数, 返回一个集合 result_selector
: 匿名函数, 接受两个参数
例子:
source = Dict(:a=>[1,2,3], :b=>[4,5])
q = source |> @mapmany(_.second, {Key=_.first, Value=__}) |> DataFrame
println(q)
宏@take、@drop、@unique
n
为整数n
为整数宏@select
选择指定列
source
可以是任何可以查询的来源。selectors...
的每个选择器都可以从source
中选择元素并将其添加到结果集中,或者从结果集中选择元素并将其删除。选择器可以通过名称、位置或使用谓词函数来选择或删除元素。
df = DataFrame(fruit=["Apple","Banana","Cherry"],
amount=[2,6,1000],
price=[1.2,2.0,0.4],
isyellow=[false,true,false])
q1 = df |> @select(2:3, occursin("ui"), -:amount) |> DataFrame
# 2:3 => :amount, :price
# occursin("ui") => :amount, :price, :fruit
# -:amount => :price, :fruit
println(q1)
宏@rename
args...
: 顺序执行的重命名操作(:raw => :new
)
q = df |> @rename(:fruit => :food, :price => :cost, :food => :name) |> DataFrame
宏@mutate
args...
: 指定元素名和值转换公式, 顺序执行
df = DataFrame(fruit=["Apple","Banana","Cherry"],
amount=[2,6,1000],
price=[1.2,2.0,0.4],
isyellow=[false,true,false])
q = df |> @mutate(price = 2 * _.price + _.amount, isyellow = _.fruit == "Apple") |> DataFrame
println(q)
宏@dropna、@dissallowna、@replacena
如果不带参数调用@dropna
, 将删除任何一列包含NA(missing)
的行。
replacement_value
是值, 简单版只适用于所有列都批量替换成同一个值的情形
完整版:
当不同的列需要不同的替换逻辑时, 要用完整版:
replacement_specifier...
: 是column_name => replacement_value
的键值对
LINQ风格的查询命令
using Query, DataFrames
# Sorting:
# @orderby <attribute>[, <attribute>]
df = DataFrame(a=[2,1,1,2,1,3],b=[2,2,1,1,3,2])
x = @from i in df begin
@orderby descending(i.a), i.b
@select i
@collect DataFrame
end
# Filtering
# @where <condition>
df = DataFrame(name=["John", "Sally", "Kirk"], age=[23., 42., 59.], children=[3,5,2])
x = @from i in df begin
@where i.age > 30. && i.children > 2
@select i
@collect DataFrame
end
# Projecting
# @select <condition>
data = [1,2,3]
x = @from i in data begin
@select i^2
@collect
end
# query中应用`{}`可以将元素转换成命名元组
df = DataFrame(name=["John", "Sally", "Kirk"], age=[23., 42., 59.], children=[3,5,2])
x = @from i in df begin
@select {i.name, Age=i.age} # 不定义名称时, 自动推断name; 显式声明名称Age;
@collect DataFrame
end
# Flattening
# 使用多个@from实现:
# @from <range_var> in <selector>
source = Dict(:a=>[1,2,3], :b=>[4,5])
q = @from i in source begin
@from j in i.second
@select {Key=i.first,Value=j}
@collect DataFrame
end
# Joining
# @join <range variable> in <source> on <left key> equals <right key>
df1 = DataFrame(a=[1,2,3], b=[1.,2.,3.])
df2 = DataFrame(c=[2,4,2], d=["John", "Jim","Sally"])
x = @from i in df1 begin
@join j in df2 on i.a equals j.c
@select {i.a,i.b,j.c,j.d}
@collect DataFrame
end
# Group join
# @join <range variable> in <source> on <left key> equals <right key> into <group variable>
df1 = DataFrame(a=[1,2,3], b=[1.,2.,3.])
df2 = DataFrame(c=[2,4,2], d=["John", "Jim","Sally"])
x = @from i in df1 begin
@join j in df2 on i.a equals j.c into k
@select {t1=i.a,t2=length(k)}
@collect DataFrame
end
# Left outer join
# @left_outer_join <range variable> in <source> on <left key> equals <right key>
source_df1 = DataFrame(a=[1,2,3], b=[1.,2.,3.])
source_df2 = DataFrame(c=[2,4,2], d=["John", "Jim","Sally"])
q = @from i in source_df1 begin
@left_outer_join j in source_df2 on i.a equals j.c
@select {i.a,i.b,j.c,j.d}
@collect DataFrame
end
# Grouping
# @group <element selector> by <key selector> [into <range variable>]
df = DataFrame(name=["John", "Sally", "Kirk"], age=[23., 42., 59.], children=[3,2,2])
x = @from i in df begin
@group i.name by i.children
@collect
end
# with into:
df = DataFrame(name=["John", "Sally", "Kirk"], age=[23., 42., 59.], children=[3,2,2])
x = @from i in df begin
@group i by i.children into g
@select {Key=key(g),Count=length(g)}
@collect DataFrame
end
# Split-Apply-Combine a.k.a dplyr
# @select new_var = agg_fun(g.var)
# agg_fun是聚合函数, 如mean, g是分组, var是要汇总的列
df = DataFrame(name=repeat(["John", "Sally", "Kirk"],
inner=[1],outer=[2]),
age=vcat([10., 20., 30.],[10., 20., 30.].+3),
children=repeat([3,2,2],inner=[1],outer=[2]),
state=[:a,:a,:a,:b,:b,:b])
x = @from i in df begin
@group i by i.state into g
@select {group=key(g),mage=mean(g.age), oldest=maximum(g.age), youngest=minimum(g.age)}
@collect DataFrame
end
# Range variables
# @let <range variable> = <value selector>
df = DataFrame(name=["John", "Sally", "Kirk"],
age=[23., 42., 59.],
children=[3,2,2])
x = @from i in df begin
@let count = length(i.name)
@let kids_per_year = i.children / i.age
@where count > 4
@select {Name=i.name, Count=count, KidsPerYear=kids_per_year}
@collect DataFrame
end
数据输出
TableType
: 可以是DataFrame
,DataTable
或者TypedTable
@collect
不带参数则输出array
类型TableType
可以是dict
, 但只在输出结果是Pair
的时候有效TableType
还可以是TimeArray
,TS
(Temporal.TS
),IndexedTable
, 具体略。
实验功能
可能会在未来版本中改动或消失的功能:
Source可以作为独立查询的第一个参数:
source |> @map(_)
和@map(source,_)
等价在独立查询命令中,
_
表示第一个参数,__
表示第二个参数, 如果同时使用了_
和__
, query会自动创建带有两个参数的匿名函数:
df_parents = DataFrame(Name=["John", "Sally"])
df_children = DataFrame(Name=["Bill", "Joe", "Mary"], Parent=["John", "John", "Sally"])
df_parents |> @join(df_children, _.Name, _.Parent, {Parent=_.Name, Child=__.Name}) |> DataFrame
@unique
的自定义选择器:source |> @unique(abs(_)) |> collect
VegaLite.jl
VegaLite.jl
是对VegaLite的封装, 旨在更方便地在julia中进行基于VegaLite的画图, 这里只介绍julia中的一些用法, VegaLite的语法不再赘述。
输入数据
DataFrame
JuliaDB
CSVFiles
VegaDatasets
VegaLite.js中没有的特性
绘图结果在julia中保存为
VLSpec
类型的对象用
@vlplot
宏或者vl
字符串宏创建VLSpec
对象重载了
load
函数, 加载VagaLite的json文件为VLSpec
重载了
save
函数, 用于输出VLSpec
为图像:
dataset("cars") |>
@vlplot(:bar, x="count()", y=:Origin) |>
save("myplot.pdf")
宏@vlplot
基本与VegaLite的json语法一致, 但是提供了一些简便的写法:
json |
julia |
移除了最外侧的
{}
冒号分隔改为用
=
键值对分隔;键不用引号, 左侧直接用字母, 右侧可以用
symbol
:mark=:bar
JSON中的
null
应该替换为Julia中的nothing
提供与python的
Altair
中类似的编码速记用法
field
和type
的映射可以直接写成field:type
:
x={field=:a, type=:ordinal}
# 可以写成
x={"a:o"}
{}
中的第一个位置参数出现 timeUnit
和aggregate
中的聚合函数, 也可以用简写语法:
# aggregate:
x={field=:foo, aggregate=:mean, type=:quantitative}
# 可以简写成:
x={"mean(foo)"} # 默认aggregate的结果就是quantitative
# timeUnit:
x={field=:foo, timeUnit=:year, type=:quantitative}
# 可以简写成:
x={"year(foo):t"}
如果字符串简写后不加其他属性,则可以省略
{}
:x="foo:q"
等同于x={field=:foo, type=:quantitative}
, 如果不想指定类型, 还可以直接用Symbol
:x=:foo
encoding
的简写encoding
可以写成enc
更简单的, 可以把
encoding
的内容直接写在顶层:@vlplot(mark=:point, x="a:o", y=:b)
mark
的简写用第一个位置参数表示
mark
:@vlplot(:point, x="a:o", y=:b)
用
{}
指定更多标记属性, 将类型作为{}
内第一个位置参数mark={:point, color=:red}
x
和y
的简写可以作为
@vlplot
的第二个和第三个位置参数传递:@vlplot(:point, :colA, :colB)
vl
字符串宏
spec = vl"""<raw Vega-Lite json>"""
Vega.printrepr
可以将JSON
转换成@vlplot
格式的作图语法输出
VLSpec
类型
VegaLite的属性可以作为VLSpec
的对应属性进行访问:
spec = data("cars") |> @vlplot(:point, x=:Acceleration, y=:Cylinders)
spec.mark
spec.encoding.x.field
# 使用Setfiled.jl/Accessors.jl的@set更改属性:
using Setfield # imports `@set` etc.
spec2 = @set spec.mark = :line
spec3 = @set spec2.encoding.y.field = "Miles_per_Gallon"
数据传入
管道:
df |> @filter(_.a>30) |> @vlplot(:point, x=:a, y=:b)
data
关键字:@vlplot(:point, data=df, x=:a, y=:b)
直接传递数据向量:
@vlplot(:line, x=1:10, y={randn(10), title="XXX"})
引用外部数据
uri
(基于URIParser.jl)和Path
(基于FilePaths.jl):
using FilePaths, URIParsing
p"xxx/yyy/zzz.csv" |> @vlplot(:point, :a, :b)
URI("https://www.xxx.com/yyy.csv") |> @vlplot(:point, :a, :b)
@vlplot(:point, data=p"subfolder/file.csv", x=:a, y=:b)
@vlplot(:point, data=URI("https://www.foo.com/bar.json"), x=:a, y=:b)
@vlplot(
:point,
data={
url=p"subfolder/foo.txt",
format={type=:csv}
},
x=:a,
y=:b
)
输出
# 根据后缀名判断输出文件格式
p |> save("figure.png")
p |> save("figure.svg")
p |> save("figure.pdf")
p |> save("figure.eps")
p |> save("figure.vegalite")
save("figure.png", p)
save("figure.svg", p)
save("figure.pdf", p)
save("figure.eps", p)
save("figure.html", p)
绘图技巧
只将必要的列传递给
@vlplot
DataVoyager.jl
将Julia的Data直接传递给Voyager进行交互式数据探索:
using DataFrames, DataVoyager
data = DataFrame(a=rand(100), b=randn(100))
v1 = data |> Voyager()
v2 = Voyager(data)
# 用 `[]` 提取作图结果:
plot1 = v1[] # plot1 is a VLSpec
# 保存和重载:
v[] |> save("figure1.vegalite")
dataset("cars") |> load("figure1.vegalite")